msg_tool\scripts\hexen_haus\archive/
wag.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::decode_to_string;
6use anyhow::{Result, anyhow};
7use std::convert::TryFrom;
8use std::io::{Read, Seek, SeekFrom};
9use std::sync::{Arc, Mutex};
10
11const WAG_SIGNATURE: &[u8; 4] = b"IAF_";
12const OFFSET_TABLE_START: u64 = 0x4A;
13const DATA_SIGNATURE: u32 = 0x4154_4144; const SECTION_IMAGE: u32 = 0x4447_4D49; const SECTION_NAME: u32 = 0x454E_4E46; #[derive(Debug)]
18pub struct HexenHausWagArchiveBuilder;
20
21impl HexenHausWagArchiveBuilder {
22 pub const fn new() -> Self {
24 HexenHausWagArchiveBuilder
25 }
26}
27
28impl ScriptBuilder for HexenHausWagArchiveBuilder {
29 fn default_encoding(&self) -> Encoding {
30 Encoding::Cp932
31 }
32
33 fn default_archive_encoding(&self) -> Option<Encoding> {
34 Some(Encoding::Cp932)
35 }
36
37 fn build_script(
38 &self,
39 buf: Vec<u8>,
40 _filename: &str,
41 _encoding: Encoding,
42 archive_encoding: Encoding,
43 config: &ExtraConfig,
44 _archive: Option<&Box<dyn Script>>,
45 ) -> Result<Box<dyn Script>> {
46 Ok(Box::new(HexenHausWagArchive::new(
47 MemReader::new(buf),
48 archive_encoding,
49 config,
50 )?))
51 }
52
53 fn build_script_from_file(
54 &self,
55 filename: &str,
56 _encoding: Encoding,
57 archive_encoding: Encoding,
58 config: &ExtraConfig,
59 _archive: Option<&Box<dyn Script>>,
60 ) -> Result<Box<dyn Script>> {
61 if filename == "-" {
62 let data = crate::utils::files::read_file(filename)?;
63 return Ok(Box::new(HexenHausWagArchive::new(
64 MemReader::new(data),
65 archive_encoding,
66 config,
67 )?));
68 }
69 let file = std::fs::File::open(filename)?;
70 let reader = std::io::BufReader::new(file);
71 Ok(Box::new(HexenHausWagArchive::new(
72 reader,
73 archive_encoding,
74 config,
75 )?))
76 }
77
78 fn build_script_from_reader(
79 &self,
80 reader: Box<dyn ReadSeek>,
81 _filename: &str,
82 _encoding: Encoding,
83 archive_encoding: Encoding,
84 config: &ExtraConfig,
85 _archive: Option<&Box<dyn Script>>,
86 ) -> Result<Box<dyn Script>> {
87 Ok(Box::new(HexenHausWagArchive::new(
88 reader,
89 archive_encoding,
90 config,
91 )?))
92 }
93
94 fn extensions(&self) -> &'static [&'static str] {
95 &["wag"]
96 }
97
98 fn script_type(&self) -> &'static ScriptType {
99 &ScriptType::HexenHausWag
100 }
101
102 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
103 if buf_len >= WAG_SIGNATURE.len() && buf.starts_with(WAG_SIGNATURE) {
104 Some(10)
105 } else {
106 None
107 }
108 }
109
110 fn is_archive(&self) -> bool {
111 true
112 }
113}
114
115#[derive(Debug, Clone)]
116struct HexenHausWagEntry {
117 name: String,
118 offset: u64,
119 size: u32,
120}
121
122#[derive(Debug)]
123pub struct HexenHausWagArchive<T: Read + Seek + std::fmt::Debug> {
125 reader: Arc<Mutex<T>>,
126 file_length: u64,
127 entries: Vec<HexenHausWagEntry>,
128}
129
130impl<T: Read + Seek + std::fmt::Debug> HexenHausWagArchive<T> {
131 pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
133 reader.seek(SeekFrom::Start(0))?;
134 let mut signature = [0u8; 4];
135 reader.read_exact(&mut signature)?;
136 if signature != *WAG_SIGNATURE {
137 return Err(anyhow!("Invalid HexenHaus WAG signature"));
138 }
139
140 reader.seek(SeekFrom::Start(6))?;
141 let file_count = reader.read_u32()?;
142 if file_count == 0 {
143 return Err(anyhow!("WAG archive contains no files"));
144 }
145
146 let file_length = reader.seek(SeekFrom::End(0))?;
147 reader.seek(SeekFrom::Start(0))?;
148
149 let reader = Arc::new(Mutex::new(reader));
150 let entry_count = file_count as usize;
151
152 let offset_table_len = entry_count
153 .checked_mul(4)
154 .ok_or_else(|| anyhow!("Offset table length overflow"))?;
155 let offset_table_len_u64 =
156 u64::try_from(offset_table_len).map_err(|_| anyhow!("Offset table length overflow"))?;
157 let offset_table_end = OFFSET_TABLE_START
158 .checked_add(offset_table_len_u64)
159 .ok_or_else(|| anyhow!("Offset table exceeds addressable range"))?;
160 if offset_table_end > file_length {
161 return Err(anyhow!("Offset table extends beyond file length"));
162 }
163
164 let mut offsets_raw = vec![0u8; offset_table_len];
165 read_decrypted_exact(&reader, OFFSET_TABLE_START, &mut offsets_raw)?;
166 if offsets_raw.len() % 4 != 0 {
167 return Err(anyhow!("Invalid offset table length"));
168 }
169
170 let mut offsets = Vec::with_capacity(entry_count);
171 let mut offsets_reader = MemReader::new(offsets_raw);
172 while !offsets_reader.is_eof() {
173 let offset = offsets_reader.read_u32()?;
174 offsets.push(offset as u64);
175 }
176
177 let mut entries = Vec::with_capacity(entry_count);
178 for offset in offsets {
179 if offset
180 .checked_add(10)
181 .map_or(true, |value| value > file_length)
182 {
183 continue;
184 }
185 let mut header_buf = [0u8; 10];
186 read_decrypted_exact(&reader, offset, &mut header_buf)?;
187 let mut header_reader = MemReaderRef::new(&header_buf);
188 let signature = header_reader.read_u32()?;
189 if signature != DATA_SIGNATURE {
190 continue;
191 }
192 let section_count = header_reader.read_u32()?;
193
194 let mut entry_name: Option<String> = None;
195 let mut data_offset = 0u64;
196 let mut data_size = 0u32;
197 let mut position = offset
198 .checked_add(10)
199 .ok_or_else(|| anyhow!("Entry position overflow"))?;
200
201 for _ in 0..section_count {
202 if position >= file_length {
203 break;
204 }
205 let mut section_sig_buf = [0u8; 4];
206 read_decrypted_exact(&reader, position, &mut section_sig_buf)?;
207 let section_signature = u32::from_le_bytes(section_sig_buf);
208 position = position
209 .checked_add(4)
210 .ok_or_else(|| anyhow!("Section position overflow"))?;
211
212 match section_signature {
213 SECTION_IMAGE => {
214 let mut size_buf = [0u8; 4];
215 read_decrypted_exact(&reader, position, &mut size_buf)?;
216 let image_size = u32::from_le_bytes(size_buf);
217 let imgd_start = position
218 .checked_sub(4)
219 .ok_or_else(|| anyhow!("Invalid IMGD start position"))?;
220 data_offset = imgd_start;
221 data_size = image_size
222 .checked_add(0x10)
223 .ok_or_else(|| anyhow!("IMGD section size overflow"))?;
224 position = position
225 .checked_add(4)
226 .ok_or_else(|| anyhow!("Section position overflow"))?;
227 position = position
228 .checked_add(u64::from(image_size))
229 .ok_or_else(|| anyhow!("Section position overflow"))?;
230 position = position
231 .checked_add(2)
232 .ok_or_else(|| anyhow!("Section position overflow"))?;
233 }
234 SECTION_NAME => {
235 let mut name_len_buf = [0u8; 4];
236 read_decrypted_exact(&reader, position, &mut name_len_buf)?;
237 let raw_name_len = u32::from_le_bytes(name_len_buf);
238 position = position
239 .checked_add(4)
240 .ok_or_else(|| anyhow!("Section position overflow"))?;
241
242 let mut skip_buf = [0u8; 2];
243 read_decrypted_exact(&reader, position, &mut skip_buf)?;
244 position = position
245 .checked_add(2)
246 .ok_or_else(|| anyhow!("Section position overflow"))?;
247
248 let name_length = raw_name_len.saturating_sub(2) as usize;
249 if name_length > 0 {
250 if position > file_length {
251 break;
252 }
253 let remaining = file_length - position;
254 let name_length_u64 = u64::try_from(name_length)
255 .map_err(|_| anyhow!("Name length overflow"))?;
256 if name_length_u64 > remaining {
257 break;
258 }
259 let mut name_buf = vec![0u8; name_length];
260 read_decrypted_exact(&reader, position, &mut name_buf)?;
261 position = position
262 .checked_add(name_length_u64)
263 .ok_or_else(|| anyhow!("Section position overflow"))?;
264 let name = decode_to_string(archive_encoding, &name_buf, true)?;
265 if !name.is_empty() {
266 entry_name = Some(name);
267 }
268 }
269
270 let mut skip_tail = [0u8; 2];
271 read_decrypted_exact(&reader, position, &mut skip_tail)?;
272 position = position
273 .checked_add(2)
274 .ok_or_else(|| anyhow!("Section position overflow"))?;
275 }
276 _ => {
277 let mut section_size_buf = [0u8; 4];
278 read_decrypted_exact(&reader, position, &mut section_size_buf)?;
279 let section_size = u32::from_le_bytes(section_size_buf);
280 position = position
281 .checked_add(4)
282 .ok_or_else(|| anyhow!("Section position overflow"))?;
283 position = position
284 .checked_add(u64::from(section_size))
285 .ok_or_else(|| anyhow!("Section position overflow"))?;
286 position = position
287 .checked_add(2)
288 .ok_or_else(|| anyhow!("Section position overflow"))?;
289 }
290 }
291 }
292
293 if data_size == 0 {
294 continue;
295 }
296 if data_offset
297 .checked_add(u64::from(data_size))
298 .map_or(true, |end| end > file_length)
299 {
300 continue;
301 }
302 if let Some(name) = entry_name {
303 if !name.is_empty() {
304 entries.push(HexenHausWagEntry {
305 name,
306 offset: data_offset,
307 size: data_size,
308 });
309 }
310 }
311 }
312
313 if entries.is_empty() {
314 return Err(anyhow!("WAG archive contains no readable entries"));
315 }
316
317 Ok(HexenHausWagArchive {
318 reader,
319 file_length,
320 entries,
321 })
322 }
323
324 fn read_decrypted_slice(&self, offset: u64, size: usize) -> Result<Vec<u8>> {
325 let requested = u64::try_from(size).map_err(|_| anyhow!("Requested size overflow"))?;
326 let length = requested.min(self.file_length.saturating_sub(offset));
327 let read_len = usize::try_from(length).map_err(|_| anyhow!("Unable to allocate buffer"))?;
328 let mut buf = vec![0u8; read_len];
329 if read_len == 0 {
330 return Ok(buf);
331 }
332 read_decrypted_exact(&self.reader, offset, &mut buf)?;
333 Ok(buf)
334 }
335}
336
337impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for HexenHausWagArchive<T> {
338 fn default_output_script_type(&self) -> OutputScriptType {
339 OutputScriptType::Json
340 }
341
342 fn default_format_type(&self) -> FormatOptions {
343 FormatOptions::None
344 }
345
346 fn is_archive(&self) -> bool {
347 true
348 }
349
350 fn iter_archive_filename<'a>(
351 &'a self,
352 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
353 Ok(Box::new(
354 self.entries.iter().map(|entry| Ok(entry.name.clone())),
355 ))
356 }
357
358 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
359 Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
360 }
361
362 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
363 if index >= self.entries.len() {
364 return Err(anyhow!(
365 "Index out of bounds: {} (total files: {})",
366 index,
367 self.entries.len()
368 ));
369 }
370 let entry = self.entries[index].clone();
371 let header =
372 self.read_decrypted_slice(entry.offset, usize::min(entry.size as usize, 16))?;
373 let typ = super::detect_script_type(&entry.name, &header);
374 Ok(Box::new(WagEntry {
375 header: entry,
376 reader: self.reader.clone(),
377 pos: 0,
378 typ,
379 }))
380 }
381}
382
383struct WagEntry<T: Read + Seek> {
384 header: HexenHausWagEntry,
385 reader: Arc<Mutex<T>>,
386 pos: u64,
387 typ: Option<ScriptType>,
388}
389
390impl<T: Read + Seek> ArchiveContent for WagEntry<T> {
391 fn name(&self) -> &str {
392 &self.header.name
393 }
394
395 fn script_type(&self) -> Option<&ScriptType> {
396 self.typ.as_ref()
397 }
398}
399
400impl<T: Read + Seek> Read for WagEntry<T> {
401 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
402 let mut reader = self.reader.lock().map_err(|e| {
403 std::io::Error::new(
404 std::io::ErrorKind::Other,
405 format!("Failed to lock mutex: {}", e),
406 )
407 })?;
408 reader.seek(SeekFrom::Start(self.header.offset + self.pos))?;
409 let total_size = u64::from(self.header.size);
410 if self.pos >= total_size {
411 return Ok(0);
412 }
413 let remaining = total_size - self.pos;
414 let remaining_usize = match usize::try_from(remaining) {
415 Ok(value) => value,
416 Err(_) => usize::MAX,
417 };
418 let to_read = remaining_usize.min(buf.len());
419 if to_read == 0 {
420 return Ok(0);
421 }
422 let bytes_read = reader.read(&mut buf[..to_read])?;
423 drop(reader);
424 for byte in &mut buf[..bytes_read] {
425 *byte = byte.rotate_right(4);
426 }
427 self.pos = self.pos.saturating_add(bytes_read as u64);
428 Ok(bytes_read)
429 }
430}
431
432impl<T: Read + Seek> Seek for WagEntry<T> {
433 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
434 let new_pos = match pos {
435 SeekFrom::Start(offset) => offset,
436 SeekFrom::End(offset) => {
437 let size = i64::from(self.header.size);
438 let target = size.checked_add(offset).ok_or_else(|| {
439 std::io::Error::new(
440 std::io::ErrorKind::InvalidInput,
441 "Seek from end exceeds file length",
442 )
443 })?;
444 if target < 0 {
445 return Err(std::io::Error::new(
446 std::io::ErrorKind::InvalidInput,
447 "Seek from end before start",
448 ));
449 }
450 target as u64
451 }
452 SeekFrom::Current(offset) => {
453 let current = i64::try_from(self.pos).map_err(|_| {
454 std::io::Error::new(
455 std::io::ErrorKind::InvalidInput,
456 "Current position overflow",
457 )
458 })?;
459 let target = current.checked_add(offset).ok_or_else(|| {
460 std::io::Error::new(
461 std::io::ErrorKind::InvalidInput,
462 "Seek from current caused overflow",
463 )
464 })?;
465 if target < 0 {
466 return Err(std::io::Error::new(
467 std::io::ErrorKind::InvalidInput,
468 "Seek from current before start",
469 ));
470 }
471 target as u64
472 }
473 };
474 self.pos = new_pos;
475 Ok(self.pos)
476 }
477
478 fn stream_position(&mut self) -> std::io::Result<u64> {
479 Ok(self.pos)
480 }
481}
482
483fn read_decrypted_exact<T: Read + Seek>(
484 reader: &Arc<Mutex<T>>,
485 offset: u64,
486 buf: &mut [u8],
487) -> Result<()> {
488 if buf.is_empty() {
489 return Ok(());
490 }
491 let mut guard = reader
492 .lock()
493 .map_err(|e| anyhow!("Failed to lock reader: {}", e))?;
494 guard.seek(SeekFrom::Start(offset))?;
495 guard.read_exact(buf)?;
496 drop(guard);
497 for byte in buf.iter_mut() {
498 *byte = byte.rotate_right(4);
499 }
500 Ok(())
501}